// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using Stride.Core.Assets;
using Stride.Core.Assets.Editor.ViewModel;
using Stride.Core.Extensions;
using Stride.Core.Presentation.Collections;
using Stride.Assets.Presentation.ViewModel;
using Stride.Assets.Scripts;

namespace Stride.Assets.Presentation.AssetEditors.VisualScriptEditor
{
    public partial class VisualScriptEditorViewModel
    {
        /// <summary>
        /// The list of diagnostics generated by background compilation.
        /// </summary>
        public ObservableList<Diagnostic> Diagnostics { get; } = new ObservableList<Diagnostic>();

        private async void Session_AssetPropertiesChanged(object sender, AssetChangedEventArgs e)
        {
            // Only continue if there was changes in this asset
            if (!e.Assets.Contains(Asset))
                return;

            await TriggerBackgroundCompilation();
        }

        private async Task TriggerBackgroundCompilation()
        {
            // TODO: Get a read-only copy from AssetChangedEventArgs (this require changes in the action stack to take immutable assets snapshots on each change)
            // Right now this is not very safe as object might be edited while it background compiles
            var assetItem = Asset.AssetItem.Clone(true);
            var asset = (VisualScriptAsset)assetItem.Asset;

            var updatedDiagnostics = new List<Diagnostic>();
            var overridableMethods = new List<ISymbol>();

            try
            {
                var generatedAbsolutePath = assetItem.GetGeneratedAbsolutePath()?.ToOSPath();
                if (generatedAbsolutePath == null)
                    return;

                // Find document
                var workspace = await StrideAssetsViewModel.Instance.Code.Workspace;
                var documentId = workspace.CurrentSolution.GetDocumentIdsWithFilePath(generatedAbsolutePath).FirstOrDefault();
                if (documentId == null)
                    return;

                // Regenerate source in the background
                var compileResult = await Task.Run(() => asset.Compile(assetItem));

                // Update document with newly generated source code
                //workspace.UpdateDocument(documentId, compileResult.SyntaxTree.GetRoot()); // Not sure why, this seem to generate compilation errors -- need to investigate
                workspace.UpdateDocument(documentId, SourceText.From(compileResult.GeneratedSource));

                var document = workspace.GetDocument(documentId);
                if (document == null)
                    return;

                // Generate diagnostics and display them
                var compilation = await document.Project.GetCompilationAsync();

                overridableMethods.Clear();

                // Update semantic model
                var syntaxTree = compilation.SyntaxTrees.FirstOrDefault(x => x.FilePath == document.FilePath);
                if (syntaxTree != null)
                {
                    SemanticModel = compilation.GetSemanticModel(syntaxTree);

                    // Compute list of overridable methods
                    var type = SemanticModel.GetDeclaredSymbol(syntaxTree.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>().FirstOrDefault());

                    // Note: Logic taken from Roslyn AbstractOverrideCompletionProvider
                    var overridableMethodsCurrentType = new List<ISymbol>();
                    var baseTypes = type.GetBaseTypes().Reverse();
                    foreach (var currentType in baseTypes)
                    {
                        // Prefer overrides in derived classes
                        RemoveOverriddenMembers(overridableMethods, currentType);

                        // Retain overridable methods
                        AddOverridableMembers(overridableMethodsCurrentType, type, currentType);
                        overridableMethods.InsertRange(0, overridableMethodsCurrentType);
                        overridableMethodsCurrentType.Clear();
                    }

                    // Don't suggest already overridden members
                    RemoveOverriddenMembers(overridableMethods, type);

                    // Process non-implemented methods in interfaces too
                    var interfaces = type.GetAllInterfacesIncludingThis();
                    foreach (var @interface in interfaces)
                    {
                        foreach (var method in @interface.GetMembers().OfType<IMethodSymbol>())
                        {
                            // If method has no implementation, add it to the list
                            if (type.FindImplementationForInterfaceMember(method) == null)
                                overridableMethodsCurrentType.Add(method);
                        }

                    }

                    overridableMethods.InsertRange(0, overridableMethodsCurrentType);
                    overridableMethodsCurrentType.Clear();
                }

                foreach (var diagnostic in compilation.GetDiagnostics())
                {
                    // Only process warnings in current file
                    if (diagnostic.Location.SourceTree.FilePath != document.FilePath)
                        continue;

                    // Find the syntax node that generated this diagnostic
                    var sourceSpan = diagnostic.Location.SourceSpan;
                    var node = compileResult.SyntaxTree.GetRoot().FindNode(sourceSpan);

                    // Find which block or link generated it
                    // Go through parent recursively until we find one
                    var currentNode = node;
                    while (currentNode != null && !currentNode.HasAnnotations("Block") && !currentNode.HasAnnotations("Link"))
                    {
                        currentNode = currentNode.Parent;
                    }

                    // No match, it's probably a global error?
                    // TODO: Log it in global pane? (if yes, maybe we should move part of diagnostics code in view model?)
                    if (currentNode == null)
                        continue;

                    var blockId = currentNode.GetAnnotations("Block").FirstOrDefault()?.Data;
                    var linkId = currentNode.GetAnnotations("Link").FirstOrDefault()?.Data;

                    updatedDiagnostics.Add(new Diagnostic
                    {
                        RoslynDiagnostic = diagnostic,
                        BlockId = blockId != null ? (Guid?)Guid.Parse(blockId) : null,
                        LinkId = linkId != null ? (Guid?)Guid.Parse(linkId) : null,
                    });
                }
            }
            finally
            {
                Dispatcher.InvokeAsync(() =>
                {
                    Diagnostics.Clear();
                    Diagnostics.AddRange(updatedDiagnostics);
                    // Keep first value (null), used to add a new method
                    OverridableMethods.RemoveRange(1, OverridableMethods.Count - 1);
                    OverridableMethods.AddRange(overridableMethods.OfType<IMethodSymbol>());
                }).Forget();
            }
        }

        #region Overridable helpers taken from Roslyn AbstractOverrideCompletionProvider
        private static void AddOverridableMembers(List<ISymbol> result, INamedTypeSymbol containingType, INamedTypeSymbol type)
        {
            foreach (var member in type.GetMembers())
            {
                if (IsOverridable(member, containingType))
                {
                    result.Add(member);
                }
            }
        }

        private static void RemoveOverriddenMembers(List<ISymbol> result, INamedTypeSymbol containingType)
        {
            foreach (var member in containingType.GetMembers())
            {
                var overriddenMember = member.OverriddenMember();
                if (overriddenMember != null)
                {
                    result.Remove(overriddenMember);
                }
            }
        }

        private static bool IsOverridable(ISymbol member, INamedTypeSymbol containingType)
        {
            if (member.IsAbstract || member.IsVirtual || member.IsOverride)
            {
                if (member.IsSealed)
                {
                    return false;
                }

                // This goes quite deep (lot of code to copy from Roslyn), currenlty ignored
                //if (!member.IsAccessibleWithin(containingType))
                //{
                //    return false;
                //}

                switch (member.Kind)
                {
                    case SymbolKind.Event:
                        return true;
                    case SymbolKind.Method:
                        return ((IMethodSymbol)member).MethodKind == MethodKind.Ordinary;
                    case SymbolKind.Property:
                        return !((IPropertySymbol)member).IsWithEvents;
                }
            }

            return false;
        }
        #endregion

        public struct Diagnostic
        {
            public Microsoft.CodeAnalysis.Diagnostic RoslynDiagnostic { get; set; }
            public Guid? BlockId { get; set; }
            public Guid? LinkId { get; set; }
        }
    }
}
